
#include "../xml/DOMDocument.h"
#include "../utils/StringUtils.h"
#include "ClusterSSHDB.h"

// Need to write path finding algorithms for determining how to get to the endpoints
// from beginning. 
// Can at some later date derive 'connection' table as a cache of real world information

// What about adding dates to the additions in the user login tables?
// That way to deconstruct, we can reverse the additions in time...

ClusterSSHDB::~ClusterSSHDB()
{
}

std::vector<char *> *ClusterSSHDB::getSetOfActiveKeysForUser(const char *sso_userid)
{
   std::vector<char *> *activekeys = sqlScanAllTextf("select distinct keyname from 'user_%s';", sso_userid);
   if ((activekeys == NULL) || (activekeys->size() == 0))
   {
      if (activekeys) delete activekeys;
      return NULL;
   }
   return activekeys;
}

char *ClusterSSHDB::UnaliasHostName(const char *maybeanalias)
{
   if ((maybeanalias == NULL) || (strlen(maybeanalias) == 0))
   {
      errprint("cannot resolve %s alias to a host name", (maybeanalias) ? "an empty" : "a NULL");
      return NULL;
   }
   char *aliasname = NULL;
   sqlGetSingleStringf(&aliasname, "select hostname from aliases where aliasname='%s';", maybeanalias);
   if (aliasname) return aliasname;
   aliasname = (char *)malloc(sizeof(char)*(strlen(maybeanalias)+1));
   strcpy(aliasname, maybeanalias);
   return aliasname;
}

char *ClusterSSHDB::GetListOfClustersForUser(const char *sso_userid)
{
   if ((sso_userid == NULL) || (strlen(sso_userid) == 0))
   {
      errprint("cannot get list of cluster with %s username", (sso_userid) ? " an empty" : "a NULL");
      return NULL;
   }

   std::vector<char *> *accessible_clusters = new std::vector<char *>();
   std::vector<char *> *all_users_hosts = sqlScanAllTextf("select hostname from 'user_%s' where keyokay='Y';", sso_userid);
   if ((all_users_hosts == NULL) || (all_users_hosts->size() == 0))
   {
      if (all_users_hosts) delete all_users_hosts;
   } else
   {
      for (int i = 0; i < all_users_hosts->size(); i++)
      {
         std::vector<char *> *isclusterpart = sqlScanRowTextf("select clustername from headnodes where hostname='%s';", all_users_hosts->at(i));
         if ((isclusterpart == NULL) || (isclusterpart->size() == 0))
         {
            if (isclusterpart) delete isclusterpart;
         } else
         {
            // Do not accept multiple hostnames connecting to clusters with the same name
            int strmatch = 0;
            for (int aq = 0; aq < accessible_clusters->size(); aq++)
               strmatch |= (strcmp(accessible_clusters->at(aq), isclusterpart->at(0)) == 0);
            if (!strmatch)
            {
               // If the clustername is so far unique, add it to the cluster list
               char *tcluster = (char *)malloc(sizeof(char)*(strlen(isclusterpart->at(0))+1));
               strcpy(tcluster, isclusterpart->at(0));
               accessible_clusters->push_back(tcluster);
            }
            StringUtils::destroyStringList(isclusterpart);
         }
      }
      StringUtils::destroyStringList(all_users_hosts);
   }
   if ((accessible_clusters == NULL) || (accessible_clusters->size() == 0))
   {
      if (accessible_clusters) delete accessible_clusters;
      return NULL;
   } else
   {
      char *returnable = StringUtils::flattenStringList(accessible_clusters, ",");
      StringUtils::destroyStringList(accessible_clusters);
      return returnable;
   }
}

char *ClusterSSHDB::GetHostForClusterForUser(const char *sso_userid, const char *clusterid, std::vector<char *> *blacklist)
{
   if ((sso_userid == NULL) || (strlen(sso_userid) == 0))
   {
      errprint("cannot get list of hostnames with cluster access with %s username", (sso_userid) ? " an empty" : "a NULL");
      return NULL;
   }
   if ((clusterid == NULL) || (strlen(clusterid) == 0))
   {
      errprint("cannot get list of hostnames with access to a cluster with %s name", (clusterid) ? " an empty" : "a NULL");
      return NULL;
   }
   std::vector<char *> *candidates = sqlScanAllTextf("select hostname from headnodes where clustername='%s';", clusterid);
   if ((candidates == NULL) || (candidates->size() == 0))
   {
      errprint("No headnode(s) found for cluster named '%s'", clusterid);
      if (candidates) delete candidates;
      return NULL;
   }
   for (int i = 0; i < candidates->size(); i++)
   {
      int blacklisted = 0;
      if (blacklist)
         for (int j = 0; j < blacklist->size(); j++)
            blacklisted |= (strcmp(blacklist->at(j), candidates->at(i)) == 0);
      if (!blacklisted)
      {
         char *usable_host = (char *)malloc(sizeof(char)*(strlen(candidates->at(i))+1));
         strcpy(usable_host, candidates->at(i));
         StringUtils::destroyStringList(candidates);
         return usable_host;
      }
   }
   StringUtils::destroyStringList(candidates);
   return NULL;
}

std::vector<char *> *ClusterSSHDB::GetPathFromSourceToCluster(const char *sso_userid, const char *clusterid, std::vector<char *> *blacklist)
{
   const char *hostname = GetHostForClusterForUser(sso_userid, clusterid, blacklist);
   if ((hostname == NULL) || (strlen(hostname) == 0))
   {
      if (hostname) free((void *)hostname);
      return NULL;
   }
   std::vector<char *> *retvec = GetPathFromSourceToHost(sso_userid, hostname, blacklist);
   free((void *)hostname);
   return retvec;
}

std::vector<char *> *ClusterSSHDB::GetPathFromSourceToHost(const char *sso_userid, const char *hostname, std::vector<char *> *blacklist)
{
   if ((sso_userid == NULL) || (strlen(sso_userid) == 0))
   {
      errprint("cannot determine routing when the username is %s", (sso_userid) ? "empty" : "NULL");
      return NULL;
   }
   if ((hostname == NULL) || (strlen(hostname) == 0))
   {
      errprint("cannot find route to %s hostname", (hostname) ? "an empty" : "a NULL");
      return NULL;
   }
   char *unaliased_hostname = UnaliasHostName(hostname);
   int blacklisted = 0;
   if (blacklist)
      for (int j = 0; j < blacklist->size(); j++)
         blacklisted |= (strcmp(blacklist->at(j), unaliased_hostname) == 0);
   if (blacklisted) { free(unaliased_hostname); return NULL; }
   std::vector<char *> *hostlist = new std::vector<char *>();
   int ishostsource = BooleanIsHostSource(unaliased_hostname);
   char *newhostname = (char *)malloc(sizeof(char)*(strlen(unaliased_hostname)+1));
   strcpy(newhostname, unaliased_hostname);
   hostlist->push_back(newhostname);
   if (ishostsource)
   {
      free(unaliased_hostname);
      return hostlist;
   }
   while (!ishostsource)
   {
      int havechosen = 0;
      std::vector<char *> *allinhosts = sqlScanAllTextf("select inhost from 'connections' where outhost='%s';", newhostname);
      // Any contenders must be accessible by this user
      for (int i = 0; i < allinhosts->size(); i++)
      {
         if ((BooleanIsHostAccessibleByUser(sso_userid, allinhosts->at(i))) && (!havechosen))
         {
            blacklisted = 0;
            if (blacklist)
               for (int j = 0; j < blacklist->size(); j++)
                  blacklisted |= (strcmp(blacklist->at(j), allinhosts->at(i)) == 0);
            if (!blacklisted)
            {
               newhostname = (char *)malloc(sizeof(char)*(strlen(allinhosts->at(i))+1));
               strcpy(newhostname, allinhosts->at(i));
               hostlist->push_back(newhostname);
               ishostsource = BooleanIsHostSource(newhostname);
               havechosen = 1;
               break;
            }
         }
      }
      if (!havechosen)
      {
         // Failed to choose a next hop - no route to host
         StringUtils::destroyStringList(hostlist);
         free(unaliased_hostname);
         return NULL;
      }
   }
   std::vector<char *> *rightwayroundhostlist = new std::vector<char *>();
   for (int i = hostlist->size(); i--; )
      rightwayroundhostlist->push_back(hostlist->at(i));
   delete hostlist;
   free(unaliased_hostname);
   return rightwayroundhostlist;
}

int ClusterSSHDB::IsTrue(const char *result)
{
   int retval = -1;
   if (strcmp(result, "True") == 0) retval = 1;
   if (strcmp(result, "TRUE") == 0) retval = 1;
   if (strcmp(result, "true") == 0) retval = 1;
   if (strcmp(result, "False") == 0) retval = 0;
   if (strcmp(result, "FALSE") == 0) retval = 0;
   if (strcmp(result, "false") == 0) retval = 0;
   if (retval == -1)
   {
      errprint("Did not recognise true/false response");
      return 0;
   }
   return retval;
}

void ClusterSSHDB::MergeXMLWithClusterDB(const char *xmlfile)
{
   DOMDocument *domd = new DOMDocument(xmlfile);
   // Find first 'host' tag. Hopefully this doesn't miss out
   // parts of the XML file if it goes too deep
   xmlNodePtr tnode = domd->findNode(domd->getRootNode(), "host");
   while (tnode != NULL)
   {
      if (strcmp((const char *)(tnode->name), "host") == 0)
      {
         char *hostname = domd->getAttribValueString(tnode, "hostname");
         if (!BooleanDoesHostExist(hostname))
         {
            char *hostport = domd->getAttribValueString(tnode, "port");
            int iport = (hostport == NULL) ? 22 : atoi(hostport);
            if (hostport) free(hostport);
            char *issource = domd->getAttribValueString(tnode, "source");
            int isource = (issource == NULL) ? 0 : IsTrue(issource);
            if (issource) free(issource);
            char *issink = domd->getAttribValueString(tnode, "sink");
            int isink = (issink == NULL) ? 0 : IsTrue(issink);
            if (issink) free(issink);
            char *clusterpart = domd->getAttribValueString(tnode, "cluster");
            if (clusterpart)
               sqlInstantExecf("insert into 'headnodes' (hostname, clustername) values ('%s', '%s');", hostname, clusterpart);
            free(clusterpart);
            sqlInstantExecf("insert into 'ssh_hosts' (hostname, port, hostfingerprint, issource, issink) values ('%s', '%d', '', '%c', '%c');",
               hostname, iport,
               (isource ? 'Y' : 'N'),
               (isink ? 'Y' : 'N'));
         }
         if (hostname) free(hostname);
      }
      if (strcmp((const char *)(tnode->name), "connection") == 0)
      {
         char *connid = domd->getAttribValueString(tnode, "id");
         if (!BooleanDoesConnectionIDExist(connid))
         {
            char *inhost = domd->getAttribValueString(tnode, "in");
            char *outhost = domd->getAttribValueString(tnode, "out");
            sqlInstantExecf("insert into 'connections' (id, inhost, outhost) values ('%s', '%s', '%s');", connid, inhost, outhost);
            free(inhost);
            free(outhost);
         }
         if (connid) free(connid);
      }
      if (strcmp((const char *)(tnode->name), "cluster") == 0)
      {
         char *clustername = domd->getAttribValueString(tnode, "name");
         if (clustername)
         {
            char *nodeweight = domd->getAttribValueString(tnode, "nodes");
            int inodeweight = (nodeweight) ? atoi(nodeweight) : 1;
            free(nodeweight);
            char *wdir = domd->getAttribValueString(tnode, "persistentkeydir");
            const char *writedir = (wdir) ? wdir : "~";
            sqlInstantExecf("insert into 'clusters' (name, nodeweight, writedir, lasthappydate) values ('%s', '%d', '%s', '');", clustername, inodeweight, writedir);
            if (wdir) free(wdir);
            free(clustername);
         }
      }
      if (strcmp((const char *)(tnode->name), "alias") == 0)
      {
         char *aliasname = domd->getAttribValueString(tnode, "aliasname");
         char *realname = domd->getAttribValueString(tnode, "realname");
         if ((realname) && (aliasname))
            sqlInstantExecf("insert into 'aliases' (aliasname, hostname) values ('%s', '%s');", aliasname, realname);
         if (realname) free(realname);
         if (aliasname) free(aliasname);
      }
      tnode = tnode->next;
   }
   delete domd;
}

int ClusterSSHDB::GetClusterNodeWeight(const char *clusterid)
{
   if ((clusterid == NULL) || (strlen(clusterid) == 0))
   {
      errprint("cannot find node weight of a cluster with %s cluster name", (clusterid) ? "an empty" : "a NULL");
      return -1;
   }
   std::vector<char *> *nw_list = sqlScanRowTextf("select nodeweight from 'clusters' where name='%s';", clusterid);
   if ((nw_list == NULL) || (nw_list->size() == 0))
   {
      errprint("could not find nodeweight of cluster '%s'", clusterid);
      if (nw_list) delete nw_list;
      return -1;
   } else
   {
      int retval = atoi(nw_list->at(0));
      StringUtils::destroyStringList(nw_list);
      return retval;
   }
}

std::vector<char *> *ClusterSSHDB::GetHostsWithBrokenKeys(const char *broker_username)
{
   std::vector<char *> *output = sqlScanAllTextf("select hostname,keyokay from 'user_%s';", broker_username);
   return output;
}

int ClusterSSHDB::BooleanCanLogInWithKey(const char *broker_username, const char *host)
{
   char *unaliased_hostname = UnaliasHostName(host);
   char *keyname = CanLogInWithKey(broker_username, unaliased_hostname);
   int retval = (keyname != NULL);
   free(keyname);
   free(unaliased_hostname);
   return retval;
}

char *ClusterSSHDB::CanLogInWithKey(const char *broker_username, const char *host)
{
   char *unaliased_hostname = UnaliasHostName(host);
   std::vector<char *> *output = sqlScanAllTextf("select keyokay,keyname from 'user_%s' where hostname='%s';", broker_username, unaliased_hostname);
   if ((output == NULL) || (output->size() == 0))
   {
      if (output) delete output;
   } else
   {
      if (strcmp(output->at(0), "Y") == 0)
      {
         char *nkeyname = (char *)malloc(sizeof(char)*(strlen(output->at(1))+1));
         strcpy(nkeyname, output->at(1));
         StringUtils::destroyStringList(output);
         free(unaliased_hostname);
         return nkeyname;
      }
      StringUtils::destroyStringList(output);
      free(unaliased_hostname);
   }
   return NULL;
}

char *ClusterSSHDB::GetLogInNameForHostForUser(const char *broker_username, const char *host)
{
   char *unaliased_hostname = UnaliasHostName(host);
   std::vector<char *> *output = sqlScanAllTextf("select username from 'user_%s' where hostname='%s';", broker_username, unaliased_hostname);
   if ((output == NULL) || (output->size() == 0))
   {
      if (output) delete output;
   } else
   {
      char *loginname = (char *)malloc(sizeof(char)*(strlen(output->at(0))+1));
      strcpy(loginname, output->at(0));
      StringUtils::destroyStringList(output);
      free(unaliased_hostname);
      return loginname;
   }
   free(unaliased_hostname);
   return NULL;
}

void ClusterSSHDB::ChangeLogInKeyNameForUser(const char *broker_username, const char *newkeyname, const char *host)
{
   char *unaliased_hostname = UnaliasHostName(host);
   sqlInstantExecf("update 'user_%s' set keyokay='Y',keyname='%s' where hostname='%s';", broker_username, newkeyname, unaliased_hostname);
   free(unaliased_hostname);
}

void ClusterSSHDB::CreateLogInDetails(const char *broker_username, const char *host, const char *host_username)
{
   char *unaliased_hostname = UnaliasHostName(host);
   sqlInstantExecf("insert into 'user_%s' (hostname, username, keyokay, keyname) values ('%s', '%s', 'N', '');",
      broker_username, unaliased_hostname, host_username);
   free(unaliased_hostname);
}

void ClusterSSHDB::DeleteLogInDetails(const char *broker_username, const char *host)
{
   char *unaliased_hostname = UnaliasHostName(host);
   sqlInstantExecf("delete from 'user_%s' where hostname='%s';", broker_username, unaliased_hostname);
   free(unaliased_hostname);
}

int ClusterSSHDB::BooleanIsHostAccessibleByUser(const char *sso_userid, const char *hostname)
{
   char *unaliased_hostname = UnaliasHostName(hostname);
   std::vector<char *> *output = sqlScanRowTextf("select hostname from 'user_%s' where hostname='%s';", sso_userid, unaliased_hostname);
   int retval = (!((output == NULL) || (output->size() == 0)));
   if (output) StringUtils::destroyStringList(output);
   free(unaliased_hostname);
   return retval;
}

int ClusterSSHDB::BooleanIsHostSource(const char *hostname)
{
   char *unaliased_hostname = UnaliasHostName(hostname);
   std::vector<char *> *output = sqlScanAllTextf("select issource from 'ssh_hosts' where hostname='%s';", unaliased_hostname);
   int retval = (((output == NULL) || (output->size() == 0)) ? 0 : (((output->at(0))[0]) == 'Y'));
   if (output) StringUtils::destroyStringList(output);
   free(unaliased_hostname);
   return retval;
}

int ClusterSSHDB::BooleanDoesHostExist(const char *hostname)
{
   char *unaliased_hostname = UnaliasHostName(hostname);
   std::vector<char *> *output = sqlScanAllTextf("select hostname from 'ssh_hosts' where hostname='%s';", unaliased_hostname);
   int retval = (!((output == NULL) || (output->size() == 0)));
   if (output) StringUtils::destroyStringList(output);
   free(unaliased_hostname);
   return retval;
}

int ClusterSSHDB::BooleanDoesConnectionIDExist(const char *connid)
{
   std::vector<char *> *output = sqlScanAllTextf("select id from 'connections' where id='%s';", connid);
   int retval = (!((output == NULL) || (output->size() == 0)));
   if (output) StringUtils::destroyStringList(output);
   return retval;
}

void ClusterSSHDB::CreateUserTable(const char *username)
{
   char *user_table_name = StringUtils::varprintf("user_%s", username);
   ensureTablesAreCreated(1,
      user_table_name, "(hostname char(256) primary key not null, username char(64), keyokay char(1), keyname char(64))");
   free(user_table_name);
}

char *ClusterSSHDB::GetKeyFingerprint(const char *hostname)
{
   char *unaliased_hostname = UnaliasHostName(hostname);
   std::vector<char *> *output = sqlScanAllTextf("select hostfingerprint from 'ssh_hosts' where hostname='%s';", unaliased_hostname);
   if ((output == NULL) || (output->size() == 0))
   {
      if (output) delete output;
   } else
   {
      char *keyfingerprint = (char *)malloc(sizeof(char)*(strlen(output->at(0))+1));
      strcpy(keyfingerprint, output->at(0));
      StringUtils::destroyStringList(output);
      free(unaliased_hostname);
      return keyfingerprint;
   }
   free(unaliased_hostname);
   return NULL;
}

void ClusterSSHDB::UpdateKeyFingerprint(const char *keyfingerprint, const char *hostname)
{
   char *unaliased_hostname = UnaliasHostName(hostname);
   sqlInstantExecf("update 'ssh_hosts' set hostfingerprint='%s' where hostname='%s';", keyfingerprint, unaliased_hostname);
   free(unaliased_hostname);
}

void ClusterSSHDB::CreateHostEntry(const char *hostname, int port)
{
   char *unaliased_hostname = UnaliasHostName(hostname);
   std::vector<char *> *output = sqlScanAllTextf("select hostname from 'ssh_hosts' where hostname='%s';", unaliased_hostname);
   if ((output == NULL) || (output->size() == 0))
   {
      if (output) delete output;
      sqlInstantExecf("insert into 'ssh_hosts' (hostname, port, hostfingerprint) values ('%s', '%d', '');",
         unaliased_hostname, port);
   } else
   {
      errprint("host entry '%s' (%s) already exists", hostname, unaliased_hostname);
      StringUtils::destroyStringList(output);
   }
   free(unaliased_hostname);
}

void ClusterSSHDB::CreateTablesIfNecessary()
{
   ensureTablesAreCreated(5,
      "ssh_hosts", "(hostname char(256) primary key not null, port decimal(6,0), hostfingerprint char(64), issource char(1), issink char(1))",
      "connections", "(id char(64) primary key not null, inhost char(256), outhost char(256))",
      "headnodes", "(hostname char(256) primary key not null, clustername char(64))",
      "clusters", "(name char(64) primary key not null, nodeweight decimal(7,0), writedir char(32), lasthappydate char(32))",
      "aliases", "(aliasname char(64) primary key not null, hostname char(64))");
}
